[IMP] plugin: search, list and inspect odev plugins - #174
Conversation
Reading a plugin manifest went through `exec_module`, which is fine for a repository the user explicitly installed but unacceptable for a manifest coming from an arbitrary GitHub repository. Add `parse_plugin_manifest()`, which extracts the name, version, description and dependencies of a plugin from the source of its manifest using `ast`, reading only the module docstring and top-level literal assignments. A source that does not declare a top-level `__version__` string is not a plugin manifest, which is the test used to tell odev plugins apart from any other repository. Also extract `plugin_module_name()`, the expression converting a plugin name to the module it is linked to under the plugins directory, repeated in four places. Claude-Session: https://claude.ai/code/session_01H9M1zJCzxpg35ijsDcPEPA
`GitConnector` requires an `organization/repository` name at construction, so there was no way to talk to the GitHub API without a repository in hand. Extract the API connection concern into a new `GithubConnector` base class, a pure move of the token, connection, authentication and (dis)connection members; `GitConnector` inherits from it and keeps its public API unchanged. The new connector exposes three operations on top of it: - `search_repositories()` searches GitHub, capped to a maximum number of results to stay within the rate limits of the search API, - `get_repository()` fetches a single repository by its full name, - `get_repository_file()` reads a file from a remote repository without cloning it. All three report a missing or unreadable result as `None` rather than raising, so callers can degrade gracefully when GitHub cannot be reached. Claude-Session: https://claude.ai/code/session_01H9M1zJCzxpg35ijsDcPEPA
The `plugin` command could only enable, disable or show a plugin whose name you already knew. There was no way to discover which plugins exist, and no way to see which ones are on the machine: `--show` without an argument only covers enabled plugins, and a plugin that was disabled keeps its clone under the repositories directory without ever being mentioned again. Add two modes to the command: - `--search [terms]` looks for plugins published on GitHub, keeping only the repositories exposing a valid manifest at their root. The `odev` keyword alone is far too noisy to be usable, so repositories are matched on both `odev` and `plugin` in their name, description or topics. Archived repositories and the template repository, which cannot be installed, are left out. `--limit` caps how many repositories are inspected. - `--list` displays every plugin available locally with its state: `enabled`, `disabled`, `missing` when the link under the plugins directory is gone, or `shadowed` when another plugin already uses its module name. That last state was previously invisible although two enabled plugins forked from one another do collide, only one of them ever being loaded. `--show` builds on the same discovery and reports the state of a plugin consistently with `--list`. A plugin that is not available locally is looked up on GitHub, so uninstalled and never-downloaded plugins are described too; plugins present on the machine are read from disk and never trigger a request. Failing to reach GitHub falls back to the information available locally instead of raising. Without an argument, `--show` details every plugin available locally rather than the enabled ones only. Searching and listing never install anything: installing remains `odev plugin --enable <organization>/<repository>`. Fix `--show <organization>/<repository>`, which always reported a plugin as disabled: the name was stripped of its organization before being compared to the enabled plugins, which are stored fully qualified. The repository name alone is now accepted as well, as long as it is not ambiguous. Claude-Session: https://claude.ai/code/session_01H9M1zJCzxpg35ijsDcPEPA
|
@brinkflew doesn't target beta is it intended ? |
|
Fixed |
Merging beta brings in the deferred imports of #176, which moved the GitHub client out of the module scope. The GithubConnector extracted here keeps them inside the methods that need them, and the connectors package registers it with the lazy exports instead of importing it eagerly. Also bumps the version to 4.31.0, one increment above beta.
…e ones #174 added a TestGithubConnectorRepositories class to this module, and both branches narrowed its imports to what they needed. Both sets of classes are kept and the imports are the union of the two. Also bumps the version to 4.31.3, one increment above the base branch.
Merge order: 1st of 4Prerequisites: none. This PR targets
What the merge commit resolves#176 moved the GitHub API client out of the module scope so importing
Verified: Remaining queue
Each of the three below targets its predecessor, so GitHub retargets them to Warning GitHub Actions has not run in this repository since 2026-07-29, so the checks here are stale. The full suite was run locally against the exact merged tree of all four: 357 passed, pre-commit clean. |
## Description `odev plugin` could only enable, disable or show a plugin whose name you already knew. This PR adds discovery to the command, and makes `--show` the detail view for every plugin odev can know about. - **`odev plugin --search [terms]`** — searches GitHub for published plugins, keeping only repositories exposing a valid manifest at their root. The `odev` keyword alone is unusable (it collides with unrelated repositories, mostly Turkish *ödev*), so repositories are matched on both `odev` and `plugin` in name, description or topics; on the current index that returns 13 real plugins out of 14 hits. Archived repositories and [the template repository](https://github.com/odoo-odev/odev-plugin-template), which cannot be installed, are left out. `--limit` caps how many repositories are inspected (20 by default), bounding the cost to `1 + N` API requests. - **`odev plugin --list`** — every plugin available locally with its state: `enabled`, `disabled` (downloaded but not linked), `missing` (link gone) or `shadowed`. That last state was previously invisible: two enabled plugins forked from one another map to the same module name and only one is ever loaded — which is the case today for `odoo-odev/odev-plugin-editor-vscode` and its `avanserv` fork. - **`odev plugin --show`** — now built on the same discovery, so it reports states consistently with `--list`. A plugin that is not on the machine is looked up on GitHub, so uninstalled and never-downloaded plugins are described too; plugins present locally are read from disk and never trigger a request. Failing to reach GitHub falls back to the local information instead of raising. Without an argument, `--show` details every local plugin rather than the enabled ones only. Searching and listing never install anything — installing remains `odev plugin --enable <organization>/<repository>`. Supporting changes: - **`parse_plugin_manifest()`** (`odev/common/odev.py`) — reads a manifest with `ast`, taking only the module docstring and top-level literal assignments. Manifests are otherwise loaded through `exec_module`, which is fine for a repository the user explicitly installed but unacceptable for arbitrary search results. A top-level `__version__` string is what identifies a repository as an odev plugin. `plugin_module_name()` is extracted at the same time, replacing the same expression repeated in four places. - **`GithubConnector`** (`odev/common/connectors/git.py`) — `GitConnector` requires an `organization/repository` at construction, so there was nowhere for a search to live. The API connection concern is extracted into a new base class (a pure move of the token, connection and authentication members; `GitConnector` inherits from it and keeps its public API), which gains `search_repositories()`, `get_repository()` and `get_repository_file()`. All three return `None` rather than raising when a result is missing or unreadable. Also fixes `--show <organization>/<repository>`, which **always** reported a plugin as disabled: the name was stripped of its organization before being compared to the enabled plugins, which are stored fully qualified. A repository name on its own is now accepted too, as long as it is not ambiguous. ## Testing - Full test suite: **165 passed** (143 → 157 → 165; 18 new tests covering the manifest parser, the connector and the three command modes), py3.14, local PostgreSQL. - `pre-commit run --all-files` clean; `basedpyright` reports no new error against the baseline. - Exercised against the real GitHub API from a live checkout: `--search`, `--search ai`, `--search` with no result, `--list`, and `--show` on an enabled plugin (no request), the template repository (template warning, no install hint), an archived plugin (archived warning), a non-existent repository, the shadowed `avanserv` fork, a bare unknown name, and with no argument. - A manifest containing `os.system(...)` and `raise SystemExit(1)` at module level is parsed with no side effect (covered by a unit test). ## Compliance - [x] I have read the [contribution guide](../docs/CONTRIBUTING.md) - [x] I made sure the documentation is up-to-date both in doctrings and the `docs` directory - [x] I have added or modified unit tests where necessary - [x] I have added new libraries to the `requirements.txt` file, if any - [x] I have incremented the version number according the [versioning guide](../../docs/contributing/versioning.md) - [x] The PR contains **my changes only** and **no other external commit** 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01H9M1zJCzxpg35ijsDcPEPA
Description
odev plugincould only enable, disable or show a plugin whose name you already knew. This PR adds discovery to the command, and makes--showthe detail view for every plugin odev can know about.odev plugin --search [terms]— searches GitHub for published plugins, keeping only repositories exposing a valid manifest at their root. Theodevkeyword alone is unusable (it collides with unrelated repositories, mostly Turkish ödev), so repositories are matched on bothodevandpluginin name, description or topics; on the current index that returns 13 real plugins out of 14 hits. Archived repositories and the template repository, which cannot be installed, are left out.--limitcaps how many repositories are inspected (20 by default), bounding the cost to1 + NAPI requests.odev plugin --list— every plugin available locally with its state:enabled,disabled(downloaded but not linked),missing(link gone) orshadowed. That last state was previously invisible: two enabled plugins forked from one another map to the same module name and only one is ever loaded — which is the case today forodoo-odev/odev-plugin-editor-vscodeand itsavanservfork.odev plugin --show— now built on the same discovery, so it reports states consistently with--list. A plugin that is not on the machine is looked up on GitHub, so uninstalled and never-downloaded plugins are described too; plugins present locally are read from disk and never trigger a request. Failing to reach GitHub falls back to the local information instead of raising. Without an argument,--showdetails every local plugin rather than the enabled ones only.Searching and listing never install anything — installing remains
odev plugin --enable <organization>/<repository>.Supporting changes:
parse_plugin_manifest()(odev/common/odev.py) — reads a manifest withast, taking only the module docstring and top-level literal assignments. Manifests are otherwise loaded throughexec_module, which is fine for a repository the user explicitly installed but unacceptable for arbitrary search results. A top-level__version__string is what identifies a repository as an odev plugin.plugin_module_name()is extracted at the same time, replacing the same expression repeated in four places.GithubConnector(odev/common/connectors/git.py) —GitConnectorrequires anorganization/repositoryat construction, so there was nowhere for a search to live. The API connection concern is extracted into a new base class (a pure move of the token, connection and authentication members;GitConnectorinherits from it and keeps its public API), which gainssearch_repositories(),get_repository()andget_repository_file(). All three returnNonerather than raising when a result is missing or unreadable.Also fixes
--show <organization>/<repository>, which always reported a plugin as disabled: the name was stripped of its organization before being compared to the enabled plugins, which are stored fully qualified. A repository name on its own is now accepted too, as long as it is not ambiguous.Testing
pre-commit run --all-filesclean;basedpyrightreports no new error against the baseline.--search,--search ai,--searchwith no result,--list, and--showon an enabled plugin (no request), the template repository (template warning, no install hint), an archived plugin (archived warning), a non-existent repository, the shadowedavanservfork, a bare unknown name, and with no argument.os.system(...)andraise SystemExit(1)at module level is parsed with no side effect (covered by a unit test).Compliance
docsdirectoryrequirements.txtfile, if any🤖 Generated with Claude Code
https://claude.ai/code/session_01H9M1zJCzxpg35ijsDcPEPA